package taskit.storage;

import java.text.ParseException;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Collections;
import java.util.Collection;
import java.io.IOException;
import java.io.Serializable;
import java.util.List;
import java.io.File;

import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.annotate.JsonIgnore;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;

import taskit.test.Log;
import taskit.customexceptions.CannotAddException;
import taskit.customexceptions.CannotDeleteException;
import taskit.customexceptions.DuplicateItemException;
import taskit.datastructures.Date;
import taskit.datastructures.Deadline;
import taskit.datastructures.Floating;
import taskit.datastructures.Schedule;
import taskit.datastructures.Task;
//@author A0098733R
public class Data implements Serializable {

	private static final long serialVersionUID = 3177417631273821762L;
	private static Data data;
	private Hashtable<String, MyTreeSet> deadlinesAndSchedules;
	private LinkedList<Floating> floatingTasks;
	private LinkedList<Floating> floatingArchive;
	private LinkedList<Task> lastAdded;

	private static String TASKIT_STORAGE_FILENAME = "taskitstorage.txt";

	private static final String MESSAGE_DUPLI_SCH = "Duplicate schedule found.";
	private static final String MESSAGE_DUPLI_DL = "Duplicate deadline found.";
	private static final String ERR_CANNOT_ADD_SCH = "Unable to add this event successfully.";
	private static final String ERR_CANNOT_ADD_DL = "Unable to add this deadline successfully.";
	private static final String ERR_CANNOT_ADD_FLOATING = "Unable to add this floating task successfully.";
	private static final String ERR_CANNOT_DELETE_SCH = "Unable to delete this event.";
	private static final String ERR_CANNOT_DELETE_DL = "Unable to delete this deadline.";
	private static final String ERR_CANNOT_DELETE_FLOATING = "Unable to delete this todo.";

	/**
	 * This enum represents the type of task which may occur on a particuar day
	 */
	public static enum TaskTypes {
		HAS_SCHEDULE, HAS_DEADLINE, HAS_DEADLINE_AND_SCHEDULE, HAS_NONE
	}

	private Data() {
		deadlinesAndSchedules = new Hashtable<String, MyTreeSet>();
		floatingTasks = new LinkedList<Floating>();
		floatingArchive = new LinkedList<Floating>();
		lastAdded = new LinkedList<Task>();
	}

	public static Data getInstance() {
		Log.add("Data.java: getInstance()");
		if (data == null) {
			data = getFromFile();
		}
		return data;
	}

	/**
	 * If a storage is found and its contents can be understood, it will create
	 * an object from it, otherwise, create a new instance of Data
	 */
	private static Data getFromFile() {
		Log.add("Data.java: getFromFile()");
		File file = new File(TASKIT_STORAGE_FILENAME);
		if (file.exists()) {
			ObjectMapper mapper = new ObjectMapper();
			try {
				Data data = mapper.readValue(file, Data.class);
				data.setAllLastAddedToFalse();
				return data;
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		Log.add("Data.java: getFromFile(): returning new Data()");
		return new Data();
	}

	public static void setFileName(String fileName) {
		TASKIT_STORAGE_FILENAME = fileName;
	}

	/**
	 * This method saves the Data object into a file
	 * 
	 * @throws IOException
	 * @throws JsonMappingException
	 * @throws JsonGenerationException
	 */
	public static void saveData() throws JsonGenerationException,
			JsonMappingException, IOException {
		Log.add("Data.java: saveData()");
		ObjectMapper mapper = new ObjectMapper();
		mapper.writeValue(new File(TASKIT_STORAGE_FILENAME), data);
		Log.add("Data.java: saveData() successful");
	}

	/**
	 * This method adds a schedule to the storage
	 * 
	 * @param schedule
	 *            The schedule to be added
	 * @throws CannotAddException
	 * @throws IOException
	 * @throws JsonMappingException
	 * @throws JsonGenerationException
	 * @throws DuplicateItemException
	 * @throws Exception
	 */
	public void add(Schedule schedule) throws CannotAddException,
			JsonGenerationException, JsonMappingException, IOException,
			DuplicateItemException {
		if (contains(schedule)) {
			throw new DuplicateItemException(MESSAGE_DUPLI_SCH);
		}
		List<Date> dates = Date.getListDates(schedule.getStartDate(),
				schedule.getEndDate());

		for (Date date : dates) {
			if (deadlinesAndSchedules.containsKey(date.toString())) {
				MyTreeSet list = deadlinesAndSchedules.get(date.toString());
				list.add(schedule);
			} else {
				MyTreeSet list = new MyTreeSet();
				list.add(schedule);
				deadlinesAndSchedules.put(date.toString(), list);
			}
		}

		if (!contains(schedule)) {
			throw new CannotAddException(ERR_CANNOT_ADD_SCH);
		}
		setFalseIsLastAdded();
		setIsLastAdded(schedule);
		saveData();
	}

	public void add(Deadline deadline) throws DuplicateItemException,
			CannotAddException, JsonGenerationException, JsonMappingException,
			IOException {
		if (contains(deadline)) {
			throw new DuplicateItemException(MESSAGE_DUPLI_DL);
		}
		String date = deadline.getEndDate().extractDate().toString();
		if (deadlinesAndSchedules.containsKey(date)) {
			MyTreeSet list = deadlinesAndSchedules.get(date);
			list.add(deadline);
		} else {
			MyTreeSet list = new MyTreeSet();
			list.add(deadline);
			deadlinesAndSchedules.put(date, list);
		}
		if (!contains(deadline)) {
			throw new CannotAddException(ERR_CANNOT_ADD_DL);
		}
		setFalseIsLastAdded();
		setIsLastAdded(deadline);
		saveData();
	}

	/**
	 * This method adds a floating task to the storage
	 * 
	 * @param floating
	 *            The floating task to be added
	 * @throws CannotAddException
	 * @throws IOException
	 * @throws JsonMappingException
	 * @throws JsonGenerationException
	 * @throws Exception
	 */
	public void add(Floating floating) throws CannotAddException,
			JsonGenerationException, JsonMappingException, IOException {
		floatingTasks.add(floating);
		if (!contains(floating)) {
			throw new CannotAddException(ERR_CANNOT_ADD_FLOATING);
		}
		setFalseIsLastAdded();
		setIsLastAdded(floating);
		saveData();
	}

	/**
	 * This method adds a floating task into the archive
	 * 
	 * @param floating
	 *            The floating task to be added to the archive
	 */
	public void addToArchive(Floating floating) {
		floating.setIsLastAdded(false);
		floatingArchive.add(floating);
	}

	/**
	 * This method deletes a schedule from the storage
	 * 
	 * @param schedule
	 *            The schedule to be deleted
	 * @throws CannotDeleteException
	 * @throws IOException
	 * @throws JsonMappingException
	 * @throws JsonGenerationException
	 * @throws Exception
	 */
	public void delete(Schedule schedule) throws CannotDeleteException,
			JsonGenerationException, JsonMappingException, IOException {
		LinkedList<Date> dates = Date.getListDates(schedule.getStartDate(),
				schedule.getEndDate());
		boolean success = true;
		for (Date date : dates) {
			MyTreeSet list = deadlinesAndSchedules.get(date.toString());
			if (list == null) {
				success = false;
				continue;
			}
			if (!list.remove(schedule)) {
				success = false;
			} else {
				if (list.isEmpty()) {
					deadlinesAndSchedules.remove(date.toString());
				}
			}
		}
		if (!success) {
			throw new CannotDeleteException(ERR_CANNOT_DELETE_SCH);
		}
		saveData();
	}

	/**
	 * This method deletes the deadline from the storage
	 * 
	 * @param deadline
	 *            The deadline to be deleted
	 * @throws CannotDeleteException
	 * @throws IOException
	 * @throws JsonMappingException
	 * @throws JsonGenerationException
	 * @throws Exception
	 */
	public void delete(Deadline deadline) throws CannotDeleteException,
			JsonGenerationException, JsonMappingException, IOException {
		String date = deadline.getEndDate().extractDate().toString();
		MyTreeSet list = deadlinesAndSchedules.get(date);
		boolean success = true;
		if (list == null) {
			success = false;
		}
		if (!list.remove(deadline)) {
			success = false;
		} else {
			if (list.isEmpty()) {
				deadlinesAndSchedules.remove(date);
			}
		}
		if (!success) {
			throw new CannotDeleteException(ERR_CANNOT_DELETE_DL);
		}
		saveData();
	}

	/**
	 * This method deletes the floating task from the storage
	 * 
	 * @param floating
	 *            The floating task to be deleted
	 * @throws CannotDeleteException
	 * @throws IOException
	 * @throws JsonMappingException
	 * @throws JsonGenerationException
	 * @throws Exception
	 */
	public void delete(Floating floating) throws CannotDeleteException,
			JsonGenerationException, JsonMappingException, IOException {
		boolean success = floatingTasks.remove(floating);
		if (!success) {
			throw new CannotDeleteException(ERR_CANNOT_DELETE_FLOATING);
		}
		saveData();
	}

	/**
	 * This method deletes the floating task from the archive
	 * 
	 * @param floating
	 *            The floating task to be deleted
	 * @throws CannotDeleteException
	 * @throws IOException
	 * @throws JsonMappingException
	 * @throws JsonGenerationException
	 * @throws Exception
	 */
	public void deleteFromArchive(Floating floating)
			throws CannotDeleteException, JsonGenerationException,
			JsonMappingException, IOException {
		if (!floatingArchive.remove(floating)) {
			throw new CannotDeleteException(ERR_CANNOT_DELETE_FLOATING);
		}
		saveData();
	}

	/**
	 * This method returns a LinkedList of all current deadlines and schedules
	 * that falls on a date
	 * 
	 * @param date
	 *            A date that falls with the start and end time of a schedule or
	 *            the date of a dateline
	 * @return A LinkedList of all deadlines and schedules that falls on a date
	 */
	public LinkedList<Deadline> getDeadlinesAndSchedules(Date date) {
		LinkedList<Deadline> list = new LinkedList<Deadline>();
		MyTreeSet set = deadlinesAndSchedules
				.get(date.extractDate().toString());
		if (set != null) {
			list.addAll(set);
		}
		return list;
	}

	/**
	 * This method returns all current deadlines and schedules tht fall on any
	 * of the dates in the argument
	 * 
	 * @param list
	 *            A list of dates
	 * @return A LinkedList of deadlines and schedules that fall on one of the
	 *         dates in a given list
	 */
	public LinkedList<Deadline> getDeadlinesAndSchedules(LinkedList<Date> list) {
		LinkedList<Deadline> returnList = new LinkedList<Deadline>();
		MyTreeSet set = new MyTreeSet();
		for (Date day : list) {
			MyTreeSet tempList = deadlinesAndSchedules.get(day.toString());
			if (tempList != null) {
				for (Deadline ds : tempList) {
					set.add(ds);
				}
			}
		}
		returnList.addAll(set);
		return returnList;
	}

	/**
	 * This method returns all current deadlines and schedules which contain a
	 * particular string in its task description
	 * 
	 * @param search
	 *            The string to be searched
	 * @return LinkedList of all current deadlines and schedules which contain
	 *         the string in their description
	 */
	public LinkedList<Deadline> getDeadlinesAndSchedules(String search) {
		LinkedList<Deadline> list = getCurrent();
		LinkedList<Deadline> returnList = new LinkedList<Deadline>();
		for (Deadline task : list) {
			if (task.getName().toLowerCase().contains(search.toLowerCase())) {
				returnList.add(task);
			}
		}
		Collections.sort(returnList);
		return returnList;
	}

	/**
	 * This method returns all current deadlines and schedules which contain a
	 * particular tag
	 * 
	 * @param search
	 *            The string to be searched
	 * @return LinkedList of all current deadlines and schedules which contain
	 *         the string in their description
	 */
	public LinkedList<Deadline> getTaggedDeadlinesAndSchedules(String tag) {
		LinkedList<Deadline> list = getCurrent();
		LinkedList<Deadline> returnList = new LinkedList<Deadline>();
		LinkedList<String> tags;
		for (Deadline task : list) {
			tags = task.getTags();
			if (tags != null) {
				for (String string : tags) {
					if (string.equals(tag)) {
						returnList.add(task);
						break;
					}
				}
			}
		}
		Collections.sort(returnList);
		return returnList;
	}

	/**
	 * This method returns all current deadlines and schedules
	 * 
	 * @return LinkedList of all current deadlines and schedules
	 */
	@JsonIgnore
	public LinkedList<Deadline> getCurrent() {
		LinkedList<Deadline> list = new LinkedList<Deadline>();
		MyTreeSet set = new MyTreeSet();
		Collection<String> dates = deadlinesAndSchedules.keySet();
		Date current = Date.currentDate().extractDate();
		for (String date : dates) {
			Date key = null;
			try {
				key = new Date(date);
			} catch (ParseException e) {
				e.printStackTrace();
			}
			if (current.compareTo(key) <= 0) {
				MyTreeSet set2 = deadlinesAndSchedules.get(date);
				set.addAll(set2);
			}
		}

		list.addAll(set);
		return list;
	}

	/**
	 * This method returns all current floating tasks
	 * 
	 * @return LinkedList of all current floating tasks
	 */
	public LinkedList<Floating> getFloatingTasks() {
		return floatingTasks;
	}

	/**
	 * This method returns all floating tasks which contain a particular string
	 * in its description
	 * 
	 * @param search
	 *            The string to be searched
	 * @return LinkedList of all floating tasks which contain the string
	 */
	public LinkedList<Floating> getFloatingTasks(String search) {
		LinkedList<Floating> list = getFloatingTasks();
		LinkedList<Floating> returnList = new LinkedList<Floating>();
		for (Floating task : list) {
			if (task.getName().toLowerCase().contains(search.toLowerCase())) {
				returnList.add(task);
			}
		}
		return returnList;
	}

	/**
	 * This method returns all floating tasks which contain a particular tag
	 * 
	 * @param tag
	 * @return LinkedList of all floating tasks which contain the string
	 */
	public LinkedList<Floating> getTaggedFloatingTasks(String tag) {
		LinkedList<Floating> list = getFloatingTasks();
		LinkedList<Floating> returnList = new LinkedList<Floating>();
		LinkedList<String> tags;
		for (Floating task : list) {
			tags = task.getTags();
			if (tags != null) {
				for (String string : tags) {
					if (string.equals(tag)) {
						returnList.add(task);
					}
				}
			}
		}
		return returnList;
	}

	/**
	 * This method returns all current floating tasks if the date is later than
	 * the current date, all floating tasks that were completed on the date if
	 * the date falls before the current date, and floating tasks that are
	 * completed on the current date as well as current floating tasks if the
	 * date is the current date
	 * 
	 * @param date
	 * @return LinkedList of floating tasks
	 */
	public LinkedList<Floating> getFloatingTasks(Date date) {
		if (date.extractDate().compareTo(Date.currentDate().extractDate()) > 0) {
			return getFloatingTasks();
		} else {
			LinkedList<Floating> list = getFloatingArchive();
			LinkedList<Floating> list2 = new LinkedList<Floating>();
			for (Floating task : list) {
				if (task.getCompleteDate().extractDate().equals(date)) {
					list2.add(task);
				}
			}
			if (date.extractDate().equals(Date.currentDate().extractDate())) {
				list2.addAll(getFloatingTasks());
			}
			return list2;
		}
	}

	/**
	 * This method returns all floating tasks that are completed on any of the
	 * dates given in the list in the parameter. If one of the dates given falls
	 * during or after the current date, all current floating tasks are included
	 * 
	 * @param list
	 *            List of dates
	 * @return LinkedList of floating tasks
	 */
	public LinkedList<Floating> getFloatingTasks(LinkedList<Date> list) {
		LinkedList<Floating> list2 = new LinkedList<Floating>();
		for (Date date : list) {
			list2.addAll(getFloatingTasks(date));
			if (date.compareTo(Date.currentDate().extractDate()) >= 0) {
				break;
			}
		}
		return list2;
	}

	/**
	 * This method return a list of all deadlines and schedules that occur
	 * before the current date
	 * 
	 * @return LinkedList of all deadlines and schedules that occur before the
	 *         current date
	 */
	@JsonIgnore
	public LinkedList<Deadline> getArchive() {
		LinkedList<Deadline> list = new LinkedList<Deadline>();
		MyTreeSet set = new MyTreeSet();
		Collection<String> dates = deadlinesAndSchedules.keySet();
		Date current = Date.currentDate().extractDate();
		for (String date : dates) {
			Date key = null;
			try {
				key = new Date(date);
			} catch (ParseException e) {
				e.printStackTrace();
			}
			if (current.compareTo(key) > 0) {
				MyTreeSet set2 = deadlinesAndSchedules.get(date);
				set.addAll(set2);
			}
		}

		list.addAll(set);
		return list;
	}

	/**
	 * This method returns all deadlines and schedules which contain a
	 * particular string
	 * 
	 * @param search
	 *            String to be searched
	 * @return LinkedList of deadlines and schedules which contain a particular
	 *         string
	 */
	public LinkedList<Deadline> getArchive(String search) {
		LinkedList<Deadline> list = getArchive();
		LinkedList<Deadline> list2 = new LinkedList<Deadline>();
		for (Deadline task : list) {
			if (task.getName().toLowerCase().contains(search.toLowerCase())) {
				list2.add(task);
			}
		}
		Collections.sort(list2);
		return list2;
	}

	/**
	 * This method returns all archived floating tasks
	 * 
	 * @return LinkedList of all archived floating tasks
	 */
	public LinkedList<Floating> getFloatingArchive() {
		return floatingArchive;
	}

	public LinkedList<Floating> getFloatingArchive(String search) {
		LinkedList<Floating> list = getFloatingArchive();
		LinkedList<Floating> list2 = new LinkedList<Floating>();
		for (Floating task : list) {
			if (task.getName().toLowerCase().contains(search.toLowerCase())) {
				list2.add(task);
			}
		}
		return list2;
	}

	/**
	 * This method checks the contents of the data structures to ensure
	 * successful adding/deleting
	 * 
	 * @return <code>true</code> if the Data instance contains a particular
	 *         task, <code>false</code> otherwise
	 */
	private boolean contains(Task task) {
		if (task instanceof Floating) {
			if (floatingTasks.contains(task)) {
				return true;
			}
		} else if (task instanceof Schedule) {
			Schedule schedule = (Schedule) task;
			List<Date> dates = Date.getListDates(schedule.getStartDate(),
					schedule.getEndDate());
			for (Date date : dates) {
				MyTreeSet list = deadlinesAndSchedules.get(date.toString());
				if (list == null) {
					return false;
				}
				if (!list.contains(task)) {
					return false;
				}
			}
			return true;
		} else if (task instanceof Deadline) {
			Deadline deadline = (Deadline) task;
			String date = deadline.getEndDate().extractDate().toString();
			MyTreeSet list = deadlinesAndSchedules.get(date);
			if (list == null) {
				return false;
			}
			if (!list.contains(deadline)) {
				return false;
			}
			return true;
		}
		return false;
	}

	/**
	 * This method sets the <code>lastAdded</code> attribute of all tasks to
	 * <code>false</code>
	 */
	private void setFalseIsLastAdded() {
		while (!lastAdded.isEmpty()) {
			Task task = lastAdded.remove();
			task.setIsLastAdded(false);
		}
	}

	/**
	 * This method sets the <code>lastAdded</code> attribute of a task to
	 * <code>true</code>
	 * 
	 * @param task
	 */
	private void setIsLastAdded(Task task) {
		task.setIsLastAdded(true);
		lastAdded.add(task);
	}

	/**
	 * This method sets the <code>lastAdded</code> attribute of all tasks to
	 * <code>false</code>, and is to be run at startup to ensure that previous
	 * improper terminations of theprogram do not corrupt the data
	 */
	private void setAllLastAddedToFalse() {
		Collection<String> dates = deadlinesAndSchedules.keySet();
		for (String date : dates) {
			MyTreeSet set = deadlinesAndSchedules.get(date);
			Iterator<Deadline> iter = set.iterator();
			while (iter.hasNext()) {
				Deadline d = iter.next();
				d.setIsLastAdded(false);
			}
		}
		Iterator<Floating> iter = getFloatingTasks().iterator();
		while (iter.hasNext()) {
			Floating floating = iter.next();
			floating.setIsLastAdded(false);
		}
		iter = getFloatingArchive().iterator();
		while (iter.hasNext()) {
			Floating floating = iter.next();
			floating.setIsLastAdded(false);
		}
		lastAdded.clear();
	}

	/*
	 * These additional methods are used by the JSON parser
	 */
	public Hashtable<String, MyTreeSet> getDeadlinesAndSchedules() {
		return deadlinesAndSchedules;
	}

	public void setDeadlinesAndSchedules(
			Hashtable<String, MyTreeSet> deadlinesAndSchedules) {
		this.deadlinesAndSchedules = deadlinesAndSchedules;
	}

	public void setFloatingTasks(LinkedList<Floating> floatingTasks) {
		this.floatingTasks = floatingTasks;
	}

	public void setFloatingArchive(LinkedList<Floating> floatingArchive) {
		this.floatingArchive = floatingArchive;
	}

	public LinkedList<Task> getLastAdded() {
		return lastAdded;
	}

	public void setLastAdded(LinkedList<Task> lastAdded) {
		this.lastAdded = lastAdded;
	}

	/*
	 * This method returns the type of tasks will occur on a particular date, to
	 * be used in the calendar in the GUI
	 */
	@JsonIgnore
	public static TaskTypes getTaskTypes(Date date) {
		MyTreeSet list = Data.getInstance().deadlinesAndSchedules.get(date
				.extractDate().toString());
		if (list == null) {
			return TaskTypes.HAS_NONE;
		} else {
			return list.getTaskTypes();
		}
	}

	/*
	 * This method ensures that all tasks have the lastAdded attribute set to
	 * false upon closing the application
	 */
	public static void close() {
		try {
			data.setFalseIsLastAdded();
			Data.saveData();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * This method clears the contents of the Data instance
	 */
	public static void clear() {
		data = new Data();
	}
}
